﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Koopakiller.Numerics.Resources;

namespace Koopakiller.Numerics
{
    /// <summary>
    /// Represents a matrix.
    /// </summary>
    public struct Matrix : INotifyPropertyChanged
    {
        #region .ctor
        /// <summary>
        /// Initializes a new instance of the <see cref="Matrix"/> structure.
        /// </summary>
        /// <param name="values">The Values to put in the matrix.</param>
        public Matrix(double[,] values)
            : this()
        {
            this.values = values;
            this.SizeX = values.GetLength(0);
            this.SizeY = values.GetLength(1);
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="Matrix"/> structure.
        /// </summary>
        /// <param name="height">The count of rows in the matrix.</param>
        /// <param name="width">The count of columns in the matrix.</param>
        public Matrix(int width, int height)
            : this()
        {
            this.values = new double[width, height];
            this.SizeX = width;
            this.SizeY = height;
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="Matrix"/> structure.
        /// </summary>
        /// <param name="stride">The count of columns in the matrix.</param>
        /// <param name="values">The values to put in the matrix.</param>
        public Matrix(int stride, params double[] values)
            : this()
        {
            this.values = values.To2D(stride);
            this.SizeX = stride;
            this.SizeY = values.Length / stride;
        }
        #endregion

        /// <summary>
        /// Provides the values.
        /// </summary>
        double[,] values;

        //!? The size is cached, because GetLength is slow
        private int _SizeX;
        private int _SizeY;

        #region properties

        /// <summary>
        /// Gets the count of columns.
        /// </summary>
        public int SizeX
        {
            get
            {
                return this._SizeX;
            }
            private set
            {
                if (this._SizeX != value)
                {
                    this._SizeX = value;
                    OnPropertyChanged("SizeX");
                }
            }
        }
        /// <summary>
        /// Gets the count of rows.
        /// </summary>
        public int SizeY
        {
            get
            {
                return this._SizeY;
            }
            private set
            {
                if (this._SizeY != value)
                {
                    this._SizeY = value;
                    OnPropertyChanged("SizeY");
                }
            }
        }

        /// <summary>
        /// Gets a value that indicates the matrix is empty.
        /// </summary>
        public bool IsEmpty
        {
            get
            {
                return this.values == null || this._SizeX == 0 || this._SizeY == 0;
            }
        }

        /// <summary>
        /// Gets a value that indicates the matrix is an identity matrix.
        /// </summary>
        public bool IsIdentity
        {
            get
            {
                for (int x = 0; x < this._SizeX; ++x)
                    for (int y = 0; y < this._SizeY; ++y)
                        if ((x == y && this.values[x, y] != 1) || this.values[x, y] != 0)
                            return false;
                return true;
            }
        }

        #endregion

        /// <summary>
        /// Resize the dimension of the matrix.
        /// </summary>
        /// <param name="newWidth">The new column count.</param>
        /// <param name="newHeight">The new row count.</param>
        /// <param name="mode">Determines how to fill new cells in a larger matrix.</param>
        public void Resize(int newWidth, int newHeight, ResizeMode mode)
        {
            double[,] newValues = new double[newWidth, newHeight];

            if (this.values == null)
            {
                this.values = new double[0, 0];
                this._SizeX = 0;
                this._SizeY = 0;
            }

            for (int x = Math.Min(newWidth, this.SizeX) - 1; x >= 0; --x)
                for (int y = Math.Min(newHeight, this.SizeY) - 1; y >= 0; --y)
                    newValues[x, y] = this.values[x, y]; //? copy data (reverse to avoid a variable/save power)

            for (int x = this.SizeX; x < newWidth; ++x) //? right side of old matrix
                for (int y = 0; y < newHeight; ++y)
                    if (mode == ResizeMode.FillOne || (mode == ResizeMode.Identity && x == y))
                        newValues[x, y] = 1;
                    else
                        newValues[x, y] = 0;

            for (int y = this.SizeY; y < newHeight; ++y) //? bottom side of old matrix, without the right side
                for (int x = 0; x < this.SizeX; ++x)
                    if (mode == ResizeMode.FillOne || (mode == ResizeMode.Identity && x == y))
                        newValues[x, y] = 1;
                    else
                        newValues[x, y] = 0;

            this.values = newValues;// set new data
            this.SizeX = newWidth;
            this.SizeY = newHeight;
        }

        /// <summary>
        /// Gets or sets a value from the matrix.
        /// </summary>
        /// <param name="x">The zero based number of column.</param>
        /// <param name="y">The zero based number of row.</param>
        /// <returns>The element in the matrix at the given position.</returns>
        public double this[int x, int y]
        {
            get
            {
                return this.values[x, y];
            }
            set
            {
                this.values[x, y] = value;
            }
        }

        /// <summary>
        /// Create an identity matrix.
        /// </summary>
        /// <param name="size">The count of columns and rows in the result.</param>
        /// <returns>A identity matrix mit <paramref name="size"/> rows and columns.</returns>
        public static Matrix CreateIdentityMatrix(int size)
        {
            if (size < 0)
                throw ExceptionHelper.GetArgumentOutOfRange("size", "minval", "0");

            Matrix result = new Matrix(size, size);
            for (int x = 0; x < size; ++x)
            {
                for (int y = 0; y < size; ++y)
                {
                    if (x == y)
                        result[x, y] = 1;
                    else
                        result[x, y] = 0;
                }
            }
            return result;
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Notify a listener about a property change.
        /// </summary>
        /// <param name="propertyName">The name of the property.</param>
        private void OnPropertyChanged(string propertyName)
        {
            var evt = this.PropertyChanged;
            if (evt != null)
                evt(this, new PropertyChangedEventArgs(propertyName));
        }
        /// <summary>
        /// Occurs when a property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region math methods

        /// <summary>
        /// Adds two matrices and returns the result.
        /// </summary>
        /// <param name="m1">The first value to add.</param>
        /// <param name="m2">The second value to add.</param>
        /// <returns>The sum of <paramref name="m1"/> an <paramref name="m2"/>.</returns>
        public static Matrix Add(Matrix m1, Matrix m2)
        {
            if (m1._SizeX != m2._SizeX || m1._SizeY != m2._SizeY)
                throw ExceptionHelper.GetArgumentOutOfRange("m1, m2", "matrixSameType", "m1", "m2");
            Matrix result = new Matrix(m1._SizeX, m1._SizeY);
            for (int x = 0; x < m1._SizeX; ++x)
                for (int y = 0; y < m1._SizeY; ++y)
                    result.values[x, y] = m1.values[x, y] + m2.values[x, y];
            return result;
        }

        /// <summary>
        /// Subtract one matrix from another and returns the result.
        /// </summary>
        /// <param name="m1">The value to subtract from (the minuend).</param>
        /// <param name="m2">The value to subtract (the subtrahend).</param>
        /// <returns>The result of subtracting <paramref name="m2"/> from <paramref name="m1"/>.</returns>
        public static Matrix Subtract(Matrix m1, Matrix m2)
        {
            if (m1._SizeX != m2._SizeX || m1._SizeY != m2._SizeY)
                throw ExceptionHelper.GetArgumentOutOfRange("m1, m2", "matrixSameType", "m1", "m2");
            Matrix result = new Matrix(m1._SizeX, m1._SizeY);
            for (int x = 0; x < m1._SizeX; ++x)
                for (int y = 0; y < m1._SizeY; ++y)
                    result.values[x, y] = m1.values[x, y] - m2.values[x, y];
            return result;
        }

        /// <summary>
        /// Negates each cell of matrix.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        /// <returns>The negation of <paramref name="matrix"/>.</returns>
        public static Matrix Negate(Matrix matrix)
        {
            return Multiply(matrix, -1);
        }

        /// <summary>
        /// Return the product of one matrix and a scalar.
        /// </summary>
        /// <param name="matrix">The matrix to multiply.</param>
        /// <param name="scalar">The scalar to multiply.</param>
        /// <returns>The product of <paramref name="matrix"/> and <paramref name="scalar"/>.</returns>
        public static Matrix Multiply(Matrix matrix, double scalar)
        {
            Matrix result = new Matrix(matrix._SizeX, matrix._SizeY);
            for (int x = 0; x < matrix._SizeX; ++x)
                for (int y = 0; y < matrix._SizeY; ++y)
                    result.values[x, y] = matrix.values[x, y] * scalar;
            return result;
        }
        /// <summary>
        /// Return the product of two matrices.
        /// </summary>
        /// <param name="m1">The first value to multiply.</param>
        /// <param name="m2">The second value to multiply.</param>
        /// <returns>The product of <paramref name="m1"/> and <paramref name="m2"/>.</returns>
        public static Matrix Multiply(Matrix m1, Matrix m2)
        {
            if (m1.SizeX != m2.SizeY)
                throw ExceptionHelper.GetArgumentOutOfRange("m1, m2", "matrixMultiplyRequirement", "");

            Matrix m = new Matrix(m1.SizeY, m2.SizeX);
            for (int x = 0; x < m.SizeX; ++x)
            {
                for (int y = 0; y < m.SizeY; ++y)
                {
                    m[x, y] = CalculateCellMultiply(m1, m2, x, y);
                }
            }

            return m;
        }

        /// <summary>
        /// Determine the determinant of a matrix.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        /// <returns>The determinant of <paramref name="matrix"/>.</returns>
        public static double Determinant(Matrix matrix)
        {
            if (matrix.SizeX != matrix.SizeY)
                throw ExceptionHelper.GetArgumentOutOfRange("matrix", "matrixSquare", "");

            switch (matrix.SizeX)
            {
                case 0:
                    throw ExceptionHelper.GetArgumentOutOfRange("matrix", "minvale", "0");
                case 1:
                    return matrix[0, 0];
                case 2:
                    return (matrix[0, 0] * matrix[1, 1]) - (matrix[1, 0] * matrix[0, 1]);
                case 3:
                    return (matrix[0, 0] * matrix[1, 1] * matrix[2, 2]) + (matrix[0, 1] * matrix[1, 2] * matrix[2, 0]) + (matrix[0, 2] * matrix[1, 0] * matrix[2, 1])
                         - (matrix[2, 0] * matrix[1, 1] * matrix[0, 2]) - (matrix[2, 1] * matrix[1, 2] * matrix[0, 0]) - (matrix[0, 1] * matrix[1, 0] * matrix[2, 2]);
                default:
                    double c = 0;
                    for (int i = 0; i < matrix.SizeX; ++i)
                    {
                        Matrix m1 = GetPart(matrix, 1, matrix.SizeX - 1, 0, matrix.SizeY - 1, i);
                        if (i % 2 == 0)
                            c += Determinant(m1) * matrix[0, i];
                        else
                            c -= Determinant(m1) * matrix[0, i];
                    }
                    return c;
            }
        }

        /// <summary>
        /// Determines a value that indicates the matrix is symmetric.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        /// <returns><c>true</c> if the matrix is symmetric; otherwise, <c>false</c>.</returns>
        public static bool IsMatrixSymmetric(Matrix matrix)
        {
            for (int x = 0; x < matrix.SizeX; ++x)
            {
                for (int y = 0; y < x; ++y) //? bottom left triangle 
                {
                    if (matrix[x, y] != matrix[y, x])
                        return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Raises the power of a matrix.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        /// <param name="exponent">The exponent to raise <paramref name="matrix"/> by.</param>
        /// <returns>The result of raising <paramref name="matrix"/> to the <paramref name="exponent"/> power.</returns>
        public static Matrix Pow(Matrix matrix, int exponent)
        {
            if (matrix.SizeX != matrix.SizeY)
                throw ExceptionHelper.GetArgumentOutOfRange("matrix", "matrixSquare", "");
            if (exponent < -1)
                throw ExceptionHelper.GetArgumentOutOfRange("exponent", "minval", "-1");
            if (exponent == -1)
                return Reciprocal(matrix);
            if (exponent == 0)
                return CreateIdentityMatrix(matrix.SizeX);
            Matrix result = matrix;
            while (--exponent > 0)
                result *= matrix;
            return result;
        }

        /// <summary>
        /// Calculate the reciprocal for a matrix.
        /// </summary>
        /// <param name="matrix">Th matrix.</param>
        /// <returns>The reciprocal of <paramref name="matrix"/>.</returns>
        public static Matrix Reciprocal(Matrix matrix)
        {
            if (matrix.SizeX != matrix.SizeY)
                throw ExceptionHelper.GetArgumentOutOfRange("matrix", "matrixSquare", "");
            if (Determinant(matrix) == 0)
                throw ExceptionHelper.GetArgumentOutOfRange("matrix", "detCantZero", "");

            Matrix result = CreateIdentityMatrix(matrix.SizeX);
            for (int x = 0; x < matrix.SizeX; ++x)
            {
                double c = matrix[x, x];// one in first column
                if (c != 1)
                {
                    for (int x2 = 0; x2 < matrix.SizeX; ++x2)// divide complete row
                    {
                        matrix[x2, x] /= c;
                        result[x2, x] /= c;
                    }
                }
                for (int y = 0; y < matrix.SizeY; ++y)// create zeros
                {
                    if (y != x)// except 1
                    {
                        c = matrix[x, y];
                        for (int x2 = 0; x2 < matrix.SizeX; ++x2)// divide complete row
                        {
                            matrix[x2, y] -= c * matrix[x2, x];
                            result[x2, y] -= c * result[x2, x];
                        }
                    }
                }
            }
            return result;
        }

        #endregion

        #region operators

        /// <summary>
        /// Adds two matrices and returns the result.
        /// </summary>
        /// <param name="m1">The first value to add.</param>
        /// <param name="m2">The second value to add.</param>
        /// <returns>The sum of <paramref name="m1"/> an <paramref name="m2"/>.</returns>
        public static Matrix operator +(Matrix m1, Matrix m2)
        {
            return Add(m1, m2);
        }

        /// <summary>
        /// Subtract one matrix from another and returns the result.
        /// </summary>
        /// <param name="m1">The value to subtract from (the minuend).</param>
        /// <param name="m2">The value to subtract (the subtrahend).</param>
        /// <returns>The result of subtracting <paramref name="m2"/> from <paramref name="m1"/>.</returns>
        public static Matrix operator -(Matrix m1, Matrix m2)
        {
            return Subtract(m1, m2);
        }

        /// <summary>
        /// Negates each cell of matrix.
        /// </summary>
        /// <param name="matrix">The matrix.</param>
        /// <returns>The negation of <paramref name="matrix"/>.</returns>
        public static Matrix operator -(Matrix matrix)
        {
            return Multiply(matrix, -1);
        }

        /// <summary>
        /// Return the product of one matrix and a scalar.
        /// </summary>
        /// <param name="matrix">The matrix to multiply.</param>
        /// <param name="scalar">The scalar to multiply.</param>
        /// <returns>The product of <paramref name="matrix"/> and <paramref name="scalar"/>.</returns>
        public static Matrix operator *(Matrix matrix, double scalar)
        {
            return Multiply(matrix, scalar);
        }
        /// <summary>
        /// Return the product of one matrix and a scalar.
        /// </summary>
        /// <param name="scalar">The scalar to multiply.</param>
        /// <param name="matrix">The matrix to multiply.</param>
        /// <returns>The product of <paramref name="matrix"/> and <paramref name="scalar"/>.</returns>
        public static Matrix operator *(double scalar, Matrix matrix)
        {
            return Multiply(matrix, scalar);
        }
        /// <summary>
        /// Return the product of two matrices.
        /// </summary>
        /// <param name="m1">The first value to multiply.</param>
        /// <param name="m2">The second value to multiply.</param>
        /// <returns>The product of <paramref name="m1"/> and <paramref name="m2"/>.</returns>
        public static Matrix operator *(Matrix m1, Matrix m2)
        {
            return Multiply(m1, m2);
        }

        /// <summary>
        /// Returns a value indicating whether a matrix is equal to another matrix. 
        /// </summary>
        /// <param name="m1">The first matrix.</param>
        /// <param name="m2">The second matrix.</param>
        /// <returns><c>true</c>, if <paramref name="m1"/> equals to <paramref name="m2"/>; otherwise <c>false</c>.</returns>
        public static bool operator ==(Matrix m1, Matrix m2)
        {
            for (int x = 0; x < m1.SizeX; ++x)
                for (int y = 0; y < m1.SizeY; ++y)
                    if (m1[x, y] != m2[x, y])
                        return false;
            return true;
        }

        /// <summary>
        /// Returns a value indicating whether a matrix is not equal to another matrix. 
        /// </summary>
        /// <param name="m1">The first matrix.</param>
        /// <param name="m2">The second matrix.</param>
        /// <returns><c>false</c>, if <paramref name="m1"/> equals to <paramref name="m2"/>; otherwise <c>true</c>.</returns>
        public static bool operator !=(Matrix m1, Matrix m2)
        {
            for (int x = 0; x < m1.SizeX; ++x)
                for (int y = 0; y < m1.SizeY; ++y)
                    if (m1[x, y] != m2[x, y])
                        return true;
            return false;
        }

        #endregion

        #region override

        /// <summary>
        /// Create a string with on line from the cells of this matrix.
        /// </summary>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("{ ");
            for (int y = 0; y < this.SizeY; ++y)
            {
                sb.Append("{ ");
                for (int x = 0; x < this.SizeX; ++x)
                {
                    sb.Append(this[x, y].ToString());
                    if (x != this.SizeX - 1)
                        sb.Append(" | ");
                }
                if (y == this.SizeY - 1)
                    sb.Append(" } ");
                else
                    sb.Append(" }, ");
            }
            sb.Append("}");
            return sb.ToString();
        }

        /// <summary>
        /// Returns the bitwise exclusive-OR combined hash code of the cells.
        /// </summary>
        /// <returns>The bitwise exclusive-OR combined hash code of the cells.</returns>
        public override int GetHashCode()
        {
            int result = 0;
            for (int x = 0; x < this._SizeX; ++x)
                for (int y = 0; y < this._SizeY; ++y)
                    result ^= this.values[x, y].GetHashCode();
            return result;
        }

        /// <summary>
        /// Returns a value indicating whether this instance is equal to a specified object.
        /// </summary>
        /// <param name="obj">An object to compare with this instance. </param>
        /// <returns><c>true</c> if obj is an instance of <see cref="Matrix"/> and equals the value of this instance; otherwise, <c>false</c>.</returns>
        public override bool Equals(object obj)
        {
            if (!(obj is Matrix) || ((Matrix)obj).SizeX != this.SizeX || ((Matrix)obj).SizeY != this.SizeY)
                return false;

            for (int x = 0; x < this._SizeX; ++x)
                for (int y = 0; y < this._SizeY; ++y)
                    if (this.values[x, y] != ((Matrix)obj).values[x, y])
                        return false;

            return true;
        }

        #endregion

        #region help methods

        /// <summary>
        /// (help method) Calculate one cell from a matrix multiplication.
        /// </summary>
        /// <param name="m1">The first matrix.</param>
        /// <param name="m2">The second matrix.</param>
        /// <param name="x">The zero based index of column.</param>
        /// <param name="y">The zero based index of row.</param>
        private static double CalculateCellMultiply(Matrix m1, Matrix m2, int x, int y)
        {
            double c = new double();
            for (int i = 0; i < m1.SizeX; ++i)
                c += m1[i, y] * m2[x, i];
            return c;
        }

        /// <summary>
        /// (help method) Copy a part of the matrix.
        /// </summary>
        /// <param name="matrix">The matrix to cut.</param>
        /// <param name="left">The zero based first column to copy.</param>
        /// <param name="right">The zero based last column to copy.</param>
        /// <param name="top">The zero based first row to copy.</param>
        /// <param name="bottom">The zero based last row to copy.</param>
        /// <param name="exceptLine">The row to except. To prevent except a line pass <c>-1</c>.</param>
        private static Matrix GetPart(Matrix matrix, int left, int right, int top, int bottom, int exceptLine)
        {
            Matrix m = new Matrix(right - left + 1, bottom - top + 1 - (exceptLine == -1 ? 0 : 1));
            for (int y = 0; y < bottom - top + (exceptLine == -1 ? 0 : 1); ++y)
            {
                if (y > exceptLine)
                    for (int x = 0; x < right - left + (exceptLine == -1 ? 0 : 1); ++x)
                    {
                        m[x, y - 1] = matrix.values[x + left, y + top];
                    }
                else if (y != exceptLine)
                    for (int x = 0; x < right - left + (exceptLine == -1 ? 0 : 1); ++x)
                    {
                        m[x, y] = matrix.values[x + left, y + top];
                    }
            }
            return m;
        }

        /// <summary>
        /// Swaps two cells of the matrix.
        /// </summary>
        private void Swap(int x1, int y1, int x2, int y2)
        {
            var c = this.values[x1, y1];
            this.values[x1, y1] = this.values[x2, y2];
            this.values[x2, y2] = c;
        }

        #endregion

        /// <summary>
        /// Flip the matrix.
        /// </summary>
        /// <param name="horz"><c>true</c> if you want to flip the matrix horizontally; otherwise, <c>false</c>.</param>
        /// <param name="vert"><c>true</c> if you want to flip the matrix vertically; otherwise, <c>false</c>.</param>
        public void Flip(bool horz, bool vert)
        {
            if (!horz && !vert) return; //? no flip

            if (horz)
                for (int x = 0; x < this._SizeX / 2; ++x)
                    for (int y = 0; y < this._SizeY; ++y)
                        Swap(x, y, this.SizeX - x - 1, y);
            if (vert)
                for (int x = 0; x < this._SizeX; ++x)
                    for (int y = 0; y < this._SizeY / 2; ++y)
                        Swap(x, y, x, this._SizeY - y - 1);
        }

        /// <summary>
        /// Rotate the matrix.
        /// </summary>
        /// <param name="rotate">The rotate direction.</param>
        public void Rotate(MatrixRotate rotate)
        {
            if (
                   rotate == MatrixRotate.Right180)
                Flip(true, true);
            else
            {
                double[,] newValues = new double[this._SizeY, this._SizeX];
                switch (rotate)
                {
                    case MatrixRotate.Right90:
                        for (int x = 0; x < this._SizeX; ++x)
                            for (int y = 0; y < this._SizeY; ++y)
                                newValues[this._SizeY - y-1, x] = this.values[x, y];
                        break;
                    case MatrixRotate.Right270:
                        for (int x = 0; x < this._SizeX; ++x)
                            for (int y = 0; y < this._SizeY; ++y)
                                newValues[y,this._SizeX - x-1] = this.values[x, y];
                        break;
                }
                this.values = newValues;
                var c = this._SizeX;
                this.SizeX = SizeY;
                this.SizeY = c;
            }
        }
    }

    /// <summary>
    /// Define the rotate modes for a matrix.
    /// </summary>
    public enum MatrixRotate
    {
        /// <summary>
        /// Rotate the matrix one time to right.
        /// </summary>
        Right90,
        /// <summary>
        /// Rotate the matrix two times to right.
        /// </summary>
        Right180,
        /// <summary>
        /// Rotate the matrix one time to left.
        /// </summary>
        Right270,
    }

    /// <summary>
    /// Define the resize modes for a matrix.
    /// </summary>
    public enum ResizeMode
    {
        /// <summary>
        /// New cells will be filled with 0.
        /// </summary>
        FillZero,
        /// <summary>
        /// New cells will be filled with 1.
        /// </summary>
        FillOne,
        /// <summary>
        /// New cells will be filled with zero except the diagonal to the bottom right. It will be filled with 1.
        /// </summary>
        Identity,
    }
}